package com.jerome.election.log;

import com.jerome.election.util.ThreadPoolFactory;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Logger class for recording log files. Unlike traditional logging, this class
 * provides simple log writing and log rolling features.
 *
 * @author Jerome
 * @date 2023/9/10
 */
public class Logger {
    private String path;
    private String logFileName;
    // Maximum file size
    private long maxFileSizeBytes = 10 * 1024 * 1024; // 10MB
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

    // File input stream
    private BufferedWriter writer;
    private LoggerHandler loggerHandler;

    /**
     * Logger constructor. By default, log rolling is disabled.
     *
     * @param path        The path to the log files.
     * @param logFileName The name of the log file.
     */
    public Logger(String path, String logFileName) throws IOException {
        this.path = path;
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        this.logFileName = path + logFileName;
        writer = new BufferedWriter(new FileWriter(this.logFileName, true));
        rLogThreadStart();
    }

    /**
     * Logger constructor. By default, log rolling is enabled.
     *
     * @param path            The path to the log files.
     * @param logFileName     The name of the log file.
     * @param maxFileSizeBytes The maximum size (in bytes) of the log file before rolling.
     */
    public Logger(String path, String logFileName, Long maxFileSizeBytes) throws IOException {
        this.path = path;
        File file = new File(path);
        if (!file.exists()) {
            file.mkdirs();
        }
        this.logFileName = path + logFileName;
        this.maxFileSizeBytes = maxFileSizeBytes;
        writer = new BufferedWriter(new FileWriter(this.logFileName, true));
        rLogThreadStart();
    }

    public synchronized void write(String info) throws IOException {
        writer.write(info);
        // todo
        // Flush the buffer every time for now; in the future, we can optimize by
        // queuing log data and flushing when I/O is stable to improve write performance.
        writer.flush();
    }

    /**
     * Flushes the stream.
     *
     * @throws IOException
     */
    private void flush() throws IOException {
        writer.flush();
    }

    /**
     * Closes the current file stream.
     *
     * @throws SecurityException
     */
    private void close() throws SecurityException, IOException {
        writer.close();
    }

    /**
     * Re-creates the stream.
     *
     * @throws SecurityException
     */
    public synchronized void reFlush() throws SecurityException, IOException {
        try {
            close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        writer = new BufferedWriter(new FileWriter(logFileName, true));
    }

    /**
     * Log rolling logic.
     *
     * @throws IOException
     */
    private synchronized void rlog() throws IOException {
        String logFileName = getLogFileName();
        if (this.logFileName.equals(logFileName)) {
            return;
        }
        close();
        File f = new File(this.logFileName);
        File t = new File(logFileName);
        f.renameTo(t);
        reFlush();
        if (loggerHandler != null) {
            loggerHandler.rlogAfter();
        }
    }

    /**
     * Get the file name. When the file size exceeds maxFileSizeBytes, the file is
     * renamed.
     *
     * @return The log file name.
     */
    private String getLogFileName() {
        long currentTime = System.currentTimeMillis();
        LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(currentTime), ZoneId.systemDefault());
        String timeStamp = DATE_TIME_FORMATTER.format(dateTime);
        Path logFilePath = Paths.get(logFileName);
        long fileSizeInBytes = tryGetFileSizeInBytes(logFilePath);
        if (fileSizeInBytes > maxFileSizeBytes) {
            return logFileName + "." + timeStamp;
        } else {
            return logFileName;
        }
    }

    /**
     * Get the size of a file in bytes.
     *
     * @param path The path to the file.
     * @return The file size in bytes.
     */
    private long tryGetFileSizeInBytes(Path path) {
        try {
            return Files.size(path);
        } catch (IOException e) {
            System.err.println("Error getting file size: " + e.getMessage());
            return -1;
        }
    }

    private void rLogThreadStart() {
        ExecutorService pool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
                new ThreadPoolFactory("logger_" + logFileName));
        // Execute log rolling every 60 seconds
        pool.submit(() -> {
            while (true) {
                try {
                    // Perform log rolling
                    rlog();
                    Thread.sleep(60000);
                } catch (InterruptedException | IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public void setLoggerHandler(LoggerHandler loggerHandler) {
        this.loggerHandler = loggerHandler;
    }
}